忍者讀書會-對應表、集合表

2022-07-01 Fri

本篇參與忍者:JavaScript 開發技巧探秘 第二版讀書會的導讀內容加上自己所蒐尋的資料後所構成的文章。

不要把物件當成對應表

觀看以下程式碼

const dictionary = {
  ja: {
    "Ninjas for hire": "レンタル用の忍者",
  },
  zh: {
    "Ninjas for hire": "忍者出租",
  },
  ko: {
    "Ninjas for hire": "고용 닌자",
  },
};
console.log(dictionary.ja["Ninjas for hire"]);
console.log(dictionary.ja["constructor"]);

乍看之下可以透過dictionary.ja["Ninjas for hire"]得到日文翻譯,但當我們想要得到constructor的日文翻譯的時候,由於我們未定義在字典裏面,預期應當得到undefined,但是constructor 是 Javascript 的保留字,因此就會印出 Javascript 的 constructor 內容。

物件的 key 只能是字串

在畫面上有兩個 HTML 的元素,我們透過 javascript 的 DOM API 得到這兩個元素後,期望變成 key 並儲存某些 data 可以觀看以下的範例

HTML 的部分

<div id="firstElement"></div>
<div id="secondElement"></div>

Javascirpt

const firstElement = document.getElementById("firstElement");
const secondElement = document.getElementById("secondElement");
const map = {};

map[firstElement] = { data: "firstElement" };

console.log(map[firstElement].data);//firstElement

map[secondElement] = { data: "secondElement" };

console.log(map[secondElement].data);//secondElement
console.log(map[firstElement].data);//secondElement

console.log(map);//[object HTMLDivElement]會被作為key

透過以下語法map[firstElement] = { data: "firstElement" };在第一次的存取當中我們如期的將 { data: "firstElement" };儲存到 map 物件裡面,但是當我們撰寫以下語法map[secondElement] = { data: "secondElement" };同樣儲存到 map 物件的時候,再次印出map[firstElement].data的時候就會發現,裡面的值也被改成{ data: "secondElement" };

這是由於將物件做為 map 物件的 key 時會隱約地使用 toString 的方法將其轉成字串形式,因此 secondElement 在儲存的時候所存取的物件 key 和 firstElement 的物件 key 都是 object HTMLDivElement,所以我們會覆寫掉原本的值。

對應表-建立第一個對應表

Map 設值

set 使用方式

myMap.set(key, value);

首先透過 new Map 建立一個 Map,我們預計將忍者物件與其家鄉物件做出對應關係,因此我們使用 Map 的 set 的方式將 key 與 value 設置到 ninjaIslandMap 中,set 需要帶入兩個參數第一個參數是 key第二個參數是 value,程式碼如下

const ninjaIslandMap = new Map();

const ninja1 = { name: "Yoshi" };
const ninja2 = { name: "Hattori" };
const ninja3 = { name: "Kuma" };

ninjaIslandMap.set(ninja1, { homeIsland: "Honshu" });
ninjaIslandMap.set(ninja2, { homeIsland: "Hokkaido" });
console.log(ninjaIslandMap);

如期的我們 console.log(ninjaIslandMap)就能看到以下鍵值對應關係了。

Map 取值

get 使用方式

myMap.get(key);

當我們要取 Map 的值的時候可以使用 get 的方法,使用方式帶入其 key 後就能得到對應的值,而我們想要得到的是其對應值的 key 為 homeIsland 的值

console.log(ninjaIslandMap.get(ninja1));//印出{homeIsland:Honshu}
console.log(ninjaIslandMap.get(ninja1).homeIsland);//印出Honshu

對未定義的鍵使用 get

在上述程式碼當中,我們未定義 ninja3 的 key 值,因此回傳 undefined

console.log(ninjaIslandMap.get(ninja3));//印出undefined

取得 Map 對應關係的數量

如果要知道 map 裡面擁有多少個 key 的時候我們可以對 size 屬性作存取

console.log(ninjaIslandMap.size);

判斷是否有該鍵值

has 使用方式

myMap.has(key); 我們可以透過 has 的方法檢查該 map 是否有某個 key

console.log(ninjaIslandMap.has(ninja1));//true
console.log(ninjaIslandMap.has(ninja2));//true
console.log(ninjaIslandMap.has(ninja3));//false

清除所有 map 的 key

clear 使用方式

我們可以透過 clear 的方式將 map 擁有的 key 值,一次清除。 myMap.clear()

當我們使用 claer 方法之後就能夠一次將 map 的內容清空,因此我們再次console.log(ninjaIslandMap);其內容的時候就會顯示如下圖

ninjaIslandMap.clear();
console.log(ninjaIslandMap);

刪除某個 map 的 key

delete 使用方式

ninjaIslandMap.delete(ninja1);

對應表-鍵的相等

我們透過內建的 location.href 取得當前的網址連結,將該值帶入然後建立出兩個 URL 物件

這時候我們分別將兩個建立出來的 URL 作為 map 物件的鍵,可以發現如期的放入成為兩個鍵值的對應關係,即便這兩個鍵的內容都是相等,由於我們沒有辦法對相等運算子進行多載(overload),因此在 Map 裡面如期的建立出兩個對應表關係。

const map = new Map();
const currentLocation = location.href;

const firstLink = new URL(currentLocation);
const secondLink = new URL(currentLocation);
console.log(firstLink);
console.log(secondLink);

map.set(firstLink, { description: "firstLink"});
map.set(secondLink, { description: "secondLink"});

補充在其他程式語言中運算子多載,指的是我們可以對某些運算子自定義一些方法,例如加法運算子在對字串和數字相加會有其對應的結果,而如果對物件與物件使用加法運算子的話,在可運算子多載的程式語言當中我們可以自定義結果。

對應表-迭代

我們如果對 map 對應表直接進行 for of 的話會得到一個陣列 其中 陣列的索引值 0 為原先建立 map 時所使用的鍵

陣列的索引值 1 為原先建立 map 時所使用的值

myMap.keys()使用方式

myMap.keys()

可以使用 map 的 keys 方法直接取得該 map 的鍵值

myMap.values()使用方式

myMap.values()

也可以使用 map 的 values()方法直接取得該 map 的值

const directory = new Map();

directory.set("Yoshi", "+81 26 6462");
directory.set("Kuma", "+81 52 2378 6462");
directory.set("Hiro", "+81 76 277 46");

console.log(directory);
for (let item of directory) {
  console.log("Key:" + item[0]);
  console.log("Value:" + item[1]);
}
for (let key of directory.keys()) {
  console.log("Key:" + key);
  console.log("Value:" + directory.get(key));
}
for (let value of directory.values()) {
  console.log("Value:" + value);
}

集合表

文氏圖(John Venn)

  • mammal 哺乳動物
  • has wings 有翅膀

哺乳類而且有翅膀的動物就是蝙蝠

資料來源

Google Doodle-文氏 180 歲紀念日作品

函式建構氏模擬集合

下面的範例透過函式建構器來模擬 Set 作用,首先我們建立一個預設 key 為 data,而透過添加 prototype 的方式來添加方法,當物件使用 Set 函式建構器建立的同時將會涵蓋下列方法

  • has
    • 判斷是否有該資料
  • add
    • 如果原 data 裡面沒有該資料的話,再加入其中
  • remove
    • 移除資料集合當中的某項資料
function Set() {
  this.data = {};
  this.length = 0;
}

Set.prototype.has = function (item) {
  return typeof this.data[item] !== "undefined";
};

Set.prototype.add = function (item) {
  if (!this.has(item)) {
    this.data[item] = true;
    this.length++;
  }
};

Set.prototype.remove = function (item) {
  if (this.has(item)) {
    delete this.data[item];
    this.length--;
  }
};

const ninjas = new Set();
ninjas.add("Hattori");
ninjas.add("Hattori");

但是這僅是模擬出來的 Set 集合,基於 Javascript 原生特性,此資料無法儲存物件和陣列(原因如同先前 Map 介紹的部分,Javascript 無法將物件做為鍵),另外也可能會有取到原型物件的風險,因此 ECMA 委員會決定了一個新的標準,Set 集合。

建立第一個集合

在下列的範例當中我們將陣列["Kuma", "Hattori", "Yagyu", "Hattori"]放入置 Set 的參數作為集合建置的初始值,由於陣列當中有重複的值是Hattori,因此我們所建立的 Set 集合當中只有Kuma、Hattori、Yagyu這三個資料項 如果集合當中未含某資料項的時候,我們新增該資料項便可成功新增至 Set 集合。反之亦然。

另外當然可以使用for of來遍歷集合

const ninjas = new Set(["Kuma", "Hattori", "Yagyu", "Hattori"]);

console.log(ninjas)//內含Kuma、Hattori、Yagyu

console.log(ninjas.has("Hattori"))
console.log(ninjas.size);

console.log(!ninjas.has("Yoshi"));
ninjas.add("Yoshi");//由於集合內無Yoshi,因此新增此筆資料

console.log(ninjas.has("Yoshi"));
console.log(ninjas.size);

console.log(ninjas.has("Kuma"));

ninjas.add("Kuma");
//由於集合內已經包含此資料向因此並無再新增
console.log(ninjas.size);

for (let item of ninjas) {
  console.log(item,ninjas);
}

集合表-聯集

我們透過以下的方式建立 warriors 集合表,由於武士 samurai 和忍者 ninjas 都被徵召,其中 Hattori 是武士也是忍者,但是他是同一個人,並不會一分為二,所以透過 Spread operator 搭配 literal 建立陣列帶入作為 Set 的參數,如期我們就能得到不重複的戰士名單了!開心

const ninjas = ["Kuma", "Hattori", "Yagyu"];
const samurai = ["Hattori", "Oda", "Tomoe"];

const warriors = new Set([...ninjas, ...samurai]);

console.log(warriors.has("Kuma"));
console.log(warriors.has("Hattori"));
console.log(warriors.has("Yagyu"));
console.log(warriors.has("Oda"));
console.log(warriors.has("Tomoe"));

console.log(warriors.size === 5);

集合表-交集

交集指的是同時出現在A集合和B集合當中的資料,以原先武士和忍者陣列中,我們想要取得是武士也是忍者的人。 下列程式碼中我們先將 ninjas 透過展開運算子再包裹成陣列來轉回陣列形式,然後使用 filter 將符合條件的元素回傳,其中條件式是在 samurai 的 Set 當中也擁有(has)該元素的元素,最後將回傳值再次透過 new Set 的方式轉成 Set 集合。

const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);

const ninjaSamurais = new Set(
  [...ninjas].filter((ninja) => samurai.has(ninja))
);

console.log(ninjaSamurais.size);
console.log(ninjaSamurais.has("Hattori"));

集合表-差集

最後一個範例是我們希望找出純粹是忍者,但不是武士的人,因此我們一樣使用 filter 的方式找出符合條件的值,透過遍歷忍者的陣列找出武士集合當中不是忍者身分的值,留下的內容就是純粹的忍者,觀看以下程式碼。

const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);

const pureNinjas = new Set(
  [...ninjas].filter((ninja) => !samurai.has(ninja))
);

console.log(pureNinjas.size);
console.log(pureNinjas.has("Kuma"));
console.log(pureNinjas.has("Yagyu"));